package com.xuecheng.content.service.impl;

import com.alibaba.fastjson.JSON;
import com.xuecheng.base.exception.CommonError;
import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.content.config.MultipartSupportConfig;
import com.xuecheng.content.feignclient.MediaServiceClient;
import com.xuecheng.content.mapper.CourseBaseMapper;
import com.xuecheng.content.mapper.CourseMarketMapper;
import com.xuecheng.content.mapper.CoursePublishMapper;
import com.xuecheng.content.mapper.CoursePublishPreMapper;
import com.xuecheng.content.model.dto.CourseBaseInfoDto;
import com.xuecheng.content.model.dto.CoursePreviewDto;
import com.xuecheng.content.model.dto.TeachplanDto;
import com.xuecheng.content.model.po.CourseBase;
import com.xuecheng.content.model.po.CourseMarket;
import com.xuecheng.content.model.po.CoursePublish;
import com.xuecheng.content.model.po.CoursePublishPre;
import com.xuecheng.content.service.CourseBaseInfoService;
import com.xuecheng.content.service.CoursePublishService;
import com.xuecheng.content.service.TeachplanService;
import com.xuecheng.messagesdk.model.po.MqMessage;
import com.xuecheng.messagesdk.service.MqMessageService;
import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class CoursePublishServiceImpl implements CoursePublishService {

    @Autowired
    private CourseBaseInfoService courseBaseInfoService;
    @Autowired
    private TeachplanService teachplanService;
    @Autowired
    private CourseMarketMapper courseMarketMapper;
    @Autowired
    private CoursePublishPreMapper coursePublishPreMapper;
    @Autowired
    private CourseBaseMapper courseBaseMapper;
    @Autowired
    private CoursePublishMapper coursePublishMapper;
    @Autowired
    private MqMessageService mqMessageService;
    @Autowired
    private CoursePublishService coursePublishService;
    @Autowired
    private MediaServiceClient mediaServiceClient;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private RedissonClient redissonClient;


    @Override
    public CoursePreviewDto getCoursePreviewInfo(Long courseId) {
        CoursePreviewDto coursePreviewDto = new CoursePreviewDto();
        CourseBaseInfoDto courseBaseInfo = courseBaseInfoService.getCourseBaseInfo(courseId);
        coursePreviewDto.setCourseBase(courseBaseInfo);

        List<TeachplanDto> teachplanTree = teachplanService.findTeachplanTree(courseId);
        coursePreviewDto.setTeachplans(teachplanTree);
        return coursePreviewDto;
    }

    @Override
    @Transactional
    public void commitAudit(Long companyId, Long courseId) {
        CoursePublishPre coursePublishPre = new CoursePublishPre();

        //约束校验  这里就是方便 修改预发布表的审核状态或课程发布的状态的时候也要同时修改课程基本信息表的审核状态和课程发布状态 方便于从一张表就可以查询到课程的信息
        CourseBase courseBase = courseBaseMapper.selectById(courseId);
        //课程审核状态
        String auditStatus = courseBase.getAuditStatus();
        //当前审核状态为已提交不允许再次提交
        if("202003".equals(auditStatus)){
            XueChengPlusException.cast("当前为等待审核状态，审核完成可以再次提交。");
        }
        //本机构只允许提交本机构的课程
        if(!courseBase.getCompanyId().equals(companyId)){
            XueChengPlusException.cast("不允许提交其它机构的课程。");
        }

        //课程图片是否填写
        if(StringUtils.isEmpty(courseBase.getPic())){
            XueChengPlusException.cast("提交失败，请上传课程图片");
        }

        //因为预发布表的四张表的总和 如果只查询CourseBase就只有一张表 CourseBaseInfoDto融合了一些其他表的字段
        CourseBaseInfoDto courseBaseInfo = courseBaseInfoService.getCourseBaseInfo(courseId);
        if(courseBaseInfo==null){
            XueChengPlusException.cast("课程找不到");
        }
        BeanUtils.copyProperties(courseBaseInfo,coursePublishPre);

        CourseMarket courseMarket = courseMarketMapper.selectById(courseId);
        String courseMarketJson = JSON.toJSONString(courseMarket);
        coursePublishPre.setMarket(courseMarketJson);

        List<TeachplanDto> teachplanTree = teachplanService.findTeachplanTree(courseId);
        if(teachplanTree.size()<=0){
            XueChengPlusException.cast("提交失败，还没有添加课程计划");
        }
        String teachplanTreeJson = JSON.toJSONString(teachplanTree);
        coursePublishPre.setTeachplan(teachplanTreeJson);
        //"202003"表示审核状态为已提交
        coursePublishPre.setStatus("202003");
        coursePublishPre.setCreateDate(LocalDateTime.now());
        coursePublishPre.setCompanyId(companyId);

        CoursePublishPre coursePublishPreObj = coursePublishPreMapper.selectById(courseId);
        //在插入预发布表的时候需要进行一个判断 如果插入的id相同是不可以进行插入的 因为id是唯一的 所以存在数据就进行更新而不是插入
        if(coursePublishPreObj==null){
            coursePublishPreMapper.insert(coursePublishPre);
        }else{
            coursePublishPreMapper.updateById(coursePublishPre);
        }
        //在课程提交审核和课程发布的时候都需要同时修改课程表的审核状态和发布状态
        courseBase.setAuditStatus("202003");
        courseBaseMapper.updateById(courseBase);
    }

    @Override
    @Transactional
    public void publish(Long companyId, Long courseId) {
        //约束校验 修改预发布表的审核状态或课程发布的状态的时候也要同时修改课程基本信息表的审核状态和课程发布状态 方便于从一张表就可以查询到课程的信息
        CourseBase courseBase = courseBaseMapper.selectById(courseId);
        //本机构只允许提交本机构的课程
        if(!courseBase.getCompanyId().equals(companyId)){
            XueChengPlusException.cast("不允许提交其它机构的课程。");
        }

        CoursePublishPre coursePublishPre = coursePublishPreMapper.selectById(courseId);
        if(coursePublishPre==null){
            XueChengPlusException.cast("课程没有审核记录，无法发布");
        }
        String status = coursePublishPre.getStatus();
        if(!status.equals("202004")){
            XueChengPlusException.cast("课程没有审核通过，不允许发布");
        }

        CoursePublish coursePublish = new CoursePublish();
        BeanUtils.copyProperties(coursePublishPre,coursePublish);

        CoursePublish coursePublishObj = coursePublishMapper.selectById(courseId);
        //由于课程预发布和课程发布表的主键一样 所以在进行插入操作的时候要判断课程发布表里面有没有预发布表的数据 如果有主键是相同的所以插不进去 就变成更新了
        if(coursePublishObj==null){
            coursePublishMapper.insert(coursePublish);
        }else{
            coursePublishMapper.updateById(coursePublish);
        }

        //向消息表写入数据 用于任务调度 由于对于任务发布来说执行器要知道同步的是哪一个课程
        //所以需要有一个标记记录需要同步的课程 所以可以建一个消息表 存的是任务调度需要同步数据的一个依据
        //假如选择发布的是101号课程 需要把101号课程发布的任务写入这张表 由于这张表和课程的发布表在同一个数据库 所以课程发布成功受到事务的控制 则消息表一定可以把发布的数据写入成功
        //能使用是因为在依赖里面引入了sdk消息的通用服务的依赖
        saveCoursePublishMessage(courseId);

        coursePublishPreMapper.deleteById(courseId);

    }


    //在CoursePublishTask的任务调度里面调用了这个方法实现课程发布页面静态化
    @Override
    public File generateCourseHtml(Long courseId) {
        //配置freemarker
        //configuration用来初始化一个 FreeMarker 的配置对象。它用于配置 FreeMarker 的行为。通过这个类可以设置模板文件的加载路径、字符编码等参数
        //Configuration.getVersion()是一个静态方法，用于获取当前 FreeMarker 版本的字符串表示。这个版本信息可能在配置中使用，用于确保 FreeMarker 的配置与当前运行的版本相匹配。
        Configuration configuration = new Configuration(Configuration.getVersion());
        File htmlFile = null;
        try {
            //加载模板
            //选指定模板路径,classpath下templates下
            //得到classpath路径
            String classpath = this.getClass().getResource("/").getPath();
            configuration.setDirectoryForTemplateLoading(new File(classpath + "/templates/"));
            //设置字符编码
            configuration.setDefaultEncoding("utf-8");

            //指定模板文件名称
            Template template = configuration.getTemplate("course_template.ftl");

            //准备数据
            CoursePreviewDto coursePreviewInfo = coursePublishService.getCoursePreviewInfo(courseId);

            Map<String, Object> map = new HashMap<>();
            map.put("model", coursePreviewInfo);

            //静态化
            //参数1：模板，参数2：数据模型 使用 FreeMarker 渲染模板，将模板和数据结合生成 HTML 内容。
            String content = FreeMarkerTemplateUtils.processTemplateIntoString(template, map);
            System.out.println(content);
            //将静态化内容输出到文件中
            InputStream inputStream = IOUtils.toInputStream(content,"utf-8");
            //输出流
            htmlFile = File.createTempFile("coursepublish", ".html");
            FileOutputStream fileOutputStream = new FileOutputStream(htmlFile);
            //将输入流中的内容复制到输出流中，即将 HTML 内容写入临时文件中。
            IOUtils.copy(inputStream, fileOutputStream);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
            return htmlFile;
    }

    @Override
    public void uploadCourseHtml(Long courseId, File file) {
        try {
            MultipartFile multipartFile = MultipartSupportConfig.getMultipartFile(file);
            //因为远程调用传输的是一个文件
            //所以内容管理微服务远程调用媒资管理微服务的文件上传接口的测试用例  feign一开始是不支持multipart这种文件格式的
            //所以有了MultipartSupportConfig这个配置类让feign支持 feign要想使用还需要在启动类上面要能扫描到feignClient才会生成代理对象
            //这里指定了上传到minio的位置 所以前端在请求以course打头的请求的时候会代理到minio的这个路径上就可以访问到静态文件了
            String upload = mediaServiceClient.upload(multipartFile, "course/"+courseId+".html");
            if(upload==null){
                log.debug("远程调用走的降级服务得到的上传结果为null，课程ID：{}",courseId);
                XueChengPlusException.cast("上传静态文件过程中出现异常");
            }
        } catch (Exception e) {
            e.printStackTrace();
            XueChengPlusException.cast("上传静态文件过程中出现异常");
        }
    }

    @Override
    public CoursePublish getCoursePublish(Long courseId) {
        CoursePublish coursePublish = coursePublishMapper.selectById(courseId);
        System.out.println("=========从数据库查询==========");
        System.out.println(coursePublish);
        return coursePublish;
    }

    //并不是在发布课程成功之后把发布课程信息写入缓存 而是在查询的时候写入的缓存
    @Override
    public  CoursePublish getCoursePublishCache(Long courseId){
        //如果使用布隆过滤器 在这里进行代码的编写 查询布隆过滤器里面的内容 如果返回0表示该id是不在hashmap里面的 同时也不在数据库里面 因为hashmap的数据来源于数据库

        //查询缓存
        //redisTemplate返回的是Object存入redis的是JSON所以查到的数据先要转成json串然后json串再转成coursePublish对象
        //在查询发布课程的时候需要先判断缓存中存不存在 不存在再从数据库查询 且把数据库查询的数据放到缓存中 这样下次查询就可以查询缓存了
        //redis是key value的结构 根据key拿出发布课程的信息  这里的get和下面的set形成对应 找不到这个key就设置一个key
        Object  jsonObj = redisTemplate.opsForValue().get("course:" + courseId);
        if(jsonObj!=null){
            System.out.println("=========从Redis缓存查询==========");
            String jsonString = jsonObj.toString();
            CoursePublish coursePublish = JSON.parseObject(jsonString, CoursePublish.class);
            return coursePublish;
        }else{
            //redissonClient.getLock.lock这个方法是redisson实现的分布式锁 这个锁实现的是ReentrantLock
            //锁的名称是为了在释放锁时能够正确地匹配到之前获取的锁，并且通过与资源相关联可以确保每个资源都有自己的锁，以提高并发操作的效率和正确性。
            RLock lock = redissonClient.getLock("coursequerylock:" + courseId);
            //实际上锁的是数据库的查询 可以把锁的范围缩小缩到查询数据库的代码块上锁即可
            lock.lock();
            try {
                //进入锁的时候应该先再次查询redis缓存 因为前面的缓存查询没有上锁 此时很多并发的线程都在数据库查询的代码块外面进行排队
                //再次检查缓存是因为在高并发下第一次检查缓存是没有加锁的 所以可能有多个线程进入到了抢锁的代码块
                //虽然只有一个线程可以抢到锁 当这个线程拿到锁查询数据库写写入缓存的时候释放锁的时候
                //由于有多个线程在外面排队那么下一个线程就会立马获取锁再去查询数据库 因此当第一个线程把数据写入缓存以后进入锁的时候还需要进行一次缓存的判空
                //这样立马进行的线程因为看到第一个线程已经写入缓存了就不会去查询数据库了 可以看看单例模式的双重检查锁
                jsonObj = redisTemplate.opsForValue().get("course:" + courseId);
                if(jsonObj!=null){
                    String jsonString = jsonObj.toString();
                    CoursePublish coursePublish = JSON.parseObject(jsonString, CoursePublish.class);
                    return coursePublish;
                }
                //从数据库查询
                CoursePublish coursePublish = getCoursePublish(courseId);
                //给redis的key设置一个命名的规则 key的命名为例如course:120
                //设置过期时间300秒 存到redis里面的是JSON字符串
                //此处查询课程的时候即使为空我们也放入到缓存中保证缓存中有数据 这样就避免了缓存的穿透
                redisTemplate.opsForValue().set("course:" + courseId, JSON.toJSONString(coursePublish),300, TimeUnit.SECONDS);
                return coursePublish;
            } finally {
                //如果使用的是setnx则在这一步需要对是不是自己的锁或则调用lua脚本实现原子性对 锁进行手动的删除
                lock.unlock();
            }
        }

}
    /**
     * @description 保存消息表记录
     * @param courseId  课程id
     */
    private void saveCoursePublishMessage(Long courseId){
        MqMessage mqMessage = mqMessageService.addMessage("course_publish", String.valueOf(courseId), null, null);
        if(mqMessage==null){
            XueChengPlusException.cast(CommonError.UNKOWN_ERROR);
        }
    }
}
